<?php
/**
 *
 *  The upload procedure looks like this:
 *  ###- set upload path
 *  - verify the status (no error, is uploaded file)
 *  - select a handler for uploaded file (eg. an image, a parsable text etc)
 *  - process upload using the handler
 *
 *  The upload handler must be instanceof UploadInterface:
 *
 */
namespace Yz\modules\Uploads;

class Action_Upload
    extends \Yz_Class
    implements \Yz_Action_Interface
{

    protected $tempName;
    protected $realName;
    /**
     *  @var callback : bool callback(string tempfilename, string realfilename)
     */
    protected $uploadCallback;
    /** The name of upload element (in $_FILES)
     *  @var string
     */
    protected $uploadName;
    /** The filesystem path to file
     *  @var string
     */
    protected $uploadPath;
    /** The file's basename after name adjustment (the final name)
     *  @var string
     */
    protected $targetFileName;
    /** The extension (with leading dot)
     *  @var string
     */
    protected $extension;
    /** The creation timestamp
     *  @var int
     */
    protected $created;


    /**************************
     *      Public methods    *
     **************************/


    public function getUploadName()
    {
        return $this->uploadName;
    }

    public function setUploadName($uploadName)
    {
        $this->uploadName = \Yz_String::testNotEmpty($uploadName);
        return $this;
    }

    /** set a callback to run on uploaded file
     *  @param callback bool callback(string tempfilename, string realfilename)
     *  @return this
     */
    public function setUploadVerifyCallback($callback)
    {
        if (! is_callable($callback)) {
            throw new \Yz_Exception_InvalidArgument;
        }
        $this->uploadCallback = $callback;
        return $this;
    }


    public function getUploadPath()
    {
        return $this->uploadPath;
    }

    public function setUploadPath($uploadPath)
    {
        $path = \Yz_String::testNotEmpty($uploadPath);
        $path = YZ_ROOT . trim($path, '/') . '/';
        if (! is_dir($path)) {
            throw new Exception_UploadFailed( 'The upload directory does not exist' );
        }
        if (! is_writable($path)) {
            throw new Exception_UploadFailed( 'The upload directory is not writable' );
        }
        $this->uploadPath = $path;
        return $this;
    }



    public function perform()
    {
        return $this->execute();
    }

    /**
     *
     *  @return
     *  @throw
     */
    public function execute()
    {
        $uploadName = $this->uploadName;
        if (null === $uploadName) {
            throw new Exception_UploadFailed("Upload name not set");
        }
        $uploadPath = $this->uploadPath;
        if (null === $uploadPath) {
            throw new Exception_UploadFailed("Upload path not set");
        }
        return $this
            ->doUpload($uploadName)
            ->verifyUpload()
            ->selectFileName($uploadPath)
            ->saveFile()
        ;
    }

    /** return 1 if file successfully uploaded, 0 otherwise
     *  @return int
     */
    public function count()
    {
        return null === $this->created ? 0 : 1;
    }

    /**
     *  @return array
     */
    public function fetch()
    {
        return array(
            'created' => $this->created,
            'filename' => $this->targetFileName,
            'originalname' => $this->realName,
            'extension' => $this->extension,
        );
    }


    /*****************************
     *      Protected methods    *
     *****************************/


    /** perform upload checks on the element of the FILES array
     *  @param string $uploadName
     *  @return this
     */
    protected function doUpload($uploadName)
    {
        if (! isset($_FILES[$uploadName])) {
            throw new Exception_UploadFailed("Failed to identify upload '{$uploadName}'");
        }
        $upload = $_FILES[$uploadName];
        assert('isset($upload["error"])');
        if ($upload['error'] !== UPLOAD_ERR_OK) {
            $this->handleUploadError($upload);
        }

        if (! is_uploaded_file($upload['tmp_name'])) {

            throw new Exception_UploadFailed("Failed to verify upload '{$uploadName}'");
        }

        $this->tempName   = $upload['tmp_name'];
        $this->realName   = $upload['name'];
        return $this;
    }


    /** select the file name for the uploaded file to be saved
     *  @param string $uploadPath
     *  @return this
     */
    protected function selectFileName($uploadPath)
    {
        $name = $this->generateFileName($this->realName);

        $ext = (string)strrchr($name, '.');
        $nameNoExt = "" === $ext ? $name : substr($name, 0, -strlen($ext));
        $n = '';
        $i = 0;
        while (true) {
            $resName = $nameNoExt . $n . $ext;
            $target = $uploadPath . $resName;
            if (! is_file($target)) {
                break;
            }
            $n = sprintf('-%03d', ++$i);
        }
        $this->targetFileName = $resName;
        $this->extension = $ext;
        return $this;
    }


    /**
     *  @param string $tmpName
     *  @param string $origName
     *  @return string the final full path of the file
     */
    protected function saveFile()
    {
        debug();
        $tmpName = $this->tempName;
        $path = $this->uploadPath;
        $resName = $this->targetFileName;
        $target = $path . $resName;

        // handle the upload depending on extension ? => callback by extension
        // default save callback:
        move_uploaded_file($tmpName, $target);
        $this->created = (int)YZ_TIME;
        return $this;
    }

    /**
     *
     *  @todo
     */
    protected function verifyUpload()
    {
        if ($this->uploadCallback) {
            $res = call_user_func($this->uploadCallback, $this->tempName, $this->realName);
            if (! $res) {
                throw new Exception_UploadFailed("Failed to upload file: callback");
            }
        }
        return $this;
    }

    /**
     *
     *  @todo
     */
    protected function handleUploadError($upload)
    {
        /* error codes
         * UPLOAD_ERR_INI_SIZE
         * UPLOAD_ERR_FORM_SIZE
         * UPLOAD_ERR_PARTIAL
         * UPLOAD_ERR_NO_FILE
         * UPLOAD_ERR_NO_TMP_DIR
         * UPLOAD_ERR_CANT_WRITE
         */
        throw new Exception_UploadFailed("Failed to upload file: error code #" . $upload['error']);
    }

    /** generate a valid file name for the uploaded file
     *  @param string $origName original file name
     *  @return string new file name
     */
    protected function generateFileName($origName)
    {
        // remove non-printable characters
        $name = preg_replace( '~[\x00-\x1f]~', '', $origName );

        // remove leading/trailing dots
        $name = trim($name, '.');

        if ( '' === $name ) {
            throw new Exception_UploadFailed("Bad file name");
        }

        // convert non-url characters
        $name = str_replace( '_', '_5F', $name );
        $name = strtr( rawurlencode($name), '%', '_' );
        if ( strlen($name) > 250 ) {
            throw new Exception_UploadFailed( 'Supplied file name too long' );
        }
        return $name;
    }

}
